Repository: vgteam/sequenceTubeMap
Pull request #245 (2023-03-27 19:51:22)

Track settings button
- Track settings and wrapper button 
- Tests and demo

Not too sure if the settings and button component should have separate tests and demos. Might be better to merge their demo and tests?


Patch:
diff --git a/package-lock.json b/package-lock.json
index 89588a2..c72766d 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -45,6 +45,7 @@
         "react-scripts": "5.0.1",
         "react-select": "^5.6.1",
         "react-select-event": "^5.5.1",
+        "reactjs-popup": "^2.0.5",
         "reactstrap": "^9.1.5",
         "uuid": "^9.0.0",
         "webpack": "5.75.0",
@@ -13936,6 +13937,18 @@
         "react-dom": ">=16.6.0"
       }
     },
+    "node_modules/reactjs-popup": {
+      "version": "2.0.5",
+      "resolved": "https://registry.npmjs.org/reactjs-popup/-/reactjs-popup-2.0.5.tgz",
+      "integrity": "sha512-b5hv9a6aGsHEHXFAgPO5s1Jw1eSkopueyUVxQewGdLgqk2eW0IVXZrPRpHR629YcgIpC2oxtX8OOZ8a7bQJbxA==",
+      "engines": {
+        "node": ">=10"
+      },
+      "peerDependencies": {
+        "react": ">=16",
+        "react-dom": ">=16"
+      }
+    },
     "node_modules/reactstrap": {
       "version": "9.1.5",
       "resolved": "https://registry.npmjs.org/reactstrap/-/reactstrap-9.1.5.tgz",
@@ -26831,6 +26844,12 @@
         "prop-types": "^15.6.2"
       }
     },
+    "reactjs-popup": {
+      "version": "2.0.5",
+      "resolved": "https://registry.npmjs.org/reactjs-popup/-/reactjs-popup-2.0.5.tgz",
+      "integrity": "sha512-b5hv9a6aGsHEHXFAgPO5s1Jw1eSkopueyUVxQewGdLgqk2eW0IVXZrPRpHR629YcgIpC2oxtX8OOZ8a7bQJbxA==",
+      "requires": {}
+    },
     "reactstrap": {
       "version": "9.1.5",
       "resolved": "https://registry.npmjs.org/reactstrap/-/reactstrap-9.1.5.tgz",
diff --git a/package.json b/package.json
index f3cd8f5..3d8cdb3 100644
--- a/package.json
+++ b/package.json
@@ -40,6 +40,7 @@
     "react-scripts": "5.0.1",
     "react-select": "^5.6.1",
     "react-select-event": "^5.5.1",
+    "reactjs-popup": "^2.0.5",
     "reactstrap": "^9.1.5",
     "uuid": "^9.0.0",
     "webpack": "5.75.0",
diff --git a/src/components/CustomizationAccordion.js b/src/components/CustomizationAccordion.js
index 0031a4f..d4a53a4 100644
--- a/src/components/CustomizationAccordion.js
+++ b/src/components/CustomizationAccordion.js
@@ -35,6 +35,12 @@ class VisualizationOptions extends Component {
     this.props.handleMappingQualityCutoffChange(event.target.value);
   };
 
+  setColorWithIndex = (index) => {
+    return (key, value) => {
+      this.props.setColorSetting(key, index, value);
+    };
+  }
+
   render() {
     const { visOptions, toggleFlag } = this.props;
     const mappingQualityOptions = Array.from(Array(61).keys()).map((i) => {
@@ -166,15 +172,13 @@ class VisualizationOptions extends Component {
                     rowHeading="Haplotypes Forward"
                     color={visOptions.colorSchemes[1].mainPallete}
                     trackType="mainPallete"
-                    index="1"
-                    setColorSetting={this.props.setColorSetting}
+                    setColorSetting={this.setColorWithIndex(1)}
                   />
                   <RadioRow
                     rowHeading="Haplotypes Reverse"
                     color={visOptions.colorSchemes[1].auxPallete}
                     trackType="auxPallete"
-                    index="1"
-                    setColorSetting={this.props.setColorSetting}
+                    setColorSetting={this.setColorWithIndex(1)}
                   />
                   {visOptions.showReads && (
                       <React.Fragment>
@@ -182,29 +186,25 @@ class VisualizationOptions extends Component {
                           rowHeading="Gam1 Forward"
                           color={visOptions.colorSchemes[2].mainPallete}
                           trackType="mainPallete"
-                          index="2"
-                          setColorSetting={this.props.setColorSetting}
+                          setColorSetting={this.setColorWithIndex(2)}
                         />
                         <RadioRow
                           rowHeading="Gam1 Reverse"
                           color={visOptions.colorSchemes[2].auxPallete}
                           trackType="auxPallete"
-                          index="2"
-                          setColorSetting={this.props.setColorSetting}
+                          setColorSetting={this.setColorWithIndex(2)}
                         />
                         <RadioRow
                           rowHeading="Gam2 Forward"
                           color={visOptions.colorSchemes[3].mainPallete}
                           trackType="mainPallete"
-                          index="3"
-                          setColorSetting={this.props.setColorSetting}
+                          setColorSetting={this.setColorWithIndex(3)}
                         />
                         <RadioRow
                           rowHeading="Gam2 Reverse"
                           color={visOptions.colorSchemes[3].auxPallete}
                           trackType="auxPallete"
-                          index="3"
-                          setColorSetting={this.props.setColorSetting}
+                          setColorSetting={this.setColorWithIndex(3)}
                         />
                       </React.Fragment>
                     )}
diff --git a/src/components/RadioRow.js b/src/components/RadioRow.js
index 5293647..493c371 100644
--- a/src/components/RadioRow.js
+++ b/src/components/RadioRow.js
@@ -2,6 +2,7 @@ import React, { Component } from "react";
 import PropTypes from "prop-types";
 import { Col, Label, Input, FormGroup } from "reactstrap";
 
+// map of all possible colors [displayedName, value]
 const colorMap = new Map([
   ["colorful", "plainColors"],
   ["greyscale", "greys"],
@@ -13,30 +14,34 @@ const colorMap = new Map([
 
 class RadioRow extends Component {
   onChange = (event) => {
+
     this.props.setColorSetting(
       this.props.trackType,
-      this.props.index,
       colorMap.get(event.target.value)
     );
+
   };
 
   render() {
     const colorRadios = Array.from(colorMap).map(([keyColor, valueColor]) => {
-      return (
-        <Col xs="auto" key={keyColor}>
-          <FormGroup check>
-            <Label check>
-              <Input
-                type="radio"
-                value={keyColor}
-                checked={this.props.color === valueColor}
-                onChange={this.onChange}
-              />
-              {keyColor}
-            </Label>
-          </FormGroup>
-        </Col>
-      );
+      if(this.props.availableColors.includes(valueColor)){
+        return (
+          <Col xs="auto" key={keyColor}>
+            <FormGroup check>
+              <Label check>
+                <Input
+                  type="radio"
+                  value={keyColor}
+                  checked={this.props.color === valueColor}
+                  onChange={this.onChange}
+                />
+                {keyColor}
+              </Label>
+            </FormGroup>
+          </Col>
+        );
+      }
+      return <></>;
     });
     return (
       <FormGroup row className="mb-1">
@@ -52,7 +57,11 @@ RadioRow.propTypes = {
   rowHeading: PropTypes.string.isRequired,
   setColorSetting: PropTypes.func.isRequired,
   trackType: PropTypes.string.isRequired,
-  index: PropTypes.number.isRequired,
+  availableColors: PropTypes.array
 };
 
+RadioRow.defaultProps = {
+  availableColors: ["greys", "ygreys", "blues", "reds", "plainColors", "lightColors"]
+}
+
 export default RadioRow;
diff --git a/src/components/TrackFilePicker.js b/src/components/TrackFilePicker.js
index 8346cdb..8f1b36f 100644
--- a/src/components/TrackFilePicker.js
+++ b/src/components/TrackFilePicker.js
@@ -1,7 +1,15 @@
 import PropTypes from "prop-types";
 import Select from "react-select";
 
-
+/*
+ * A selection dropdown component that select files.
+ * Expects a file type object in the form of {"name": string, "type": string}
+ * 
+ * The handleInputChange function expects to be passed an option type object in the form of
+ * {"label": string, "value": file}
+ *  
+ * See demo and test file for examples of this component.
+ */
 
 //handleInputChange takes in a string trackType
 export const TrackFilePicker = ({
diff --git a/src/components/TrackFilePicker.test.js b/src/components/TrackFilePicker.test.js
old mode 100755
new mode 100644
diff --git a/src/components/TrackSettings.demo.js b/src/components/TrackSettings.demo.js
new file mode 100644
index 0000000..f334588
--- /dev/null
+++ b/src/components/TrackSettings.demo.js
@@ -0,0 +1,27 @@
+import Demo, {props as P} from 'react-demo'
+// See https://github.com/rpominov/react-demo for how to make a demo
+
+import TrackSettings from "./TrackSettings.js";
+
+export default (<Demo
+    props={{
+      fileType: P.choices(["haplotype", "graph", "read"]),
+      trackColorSettings: P.json({    
+        mainPallete: "blues",
+        auxPallete: "reds",
+        colorReadsByMappingQuality: false}),
+      availableColors: P.json(["greys", "ygreys", "blues", "reds", "plainColors"])
+    }}
+  >
+    {
+      (props, update) => {
+        return <TrackSettings {...props} setTrackColorSetting={(key, value) => {
+          // modify a key in TrackColorSettings
+          let newTrackColorSettings = {...props.trackColorSettings};
+          newTrackColorSettings[key] = value;
+          update({trackColorSettings: newTrackColorSettings})
+        }}/>
+      }
+    }
+  </Demo>)
+  
diff --git a/src/components/TrackSettings.js b/src/components/TrackSettings.js
new file mode 100644
index 0000000..3d9f387
--- /dev/null
+++ b/src/components/TrackSettings.js
@@ -0,0 +1,97 @@
+import React from 'react';
+import PropTypes from "prop-types";
+import {
+    Container,
+    CardBody,
+    Card,
+    Form,
+  } from "reactstrap";
+import RadioRow from "./RadioRow";
+
+/**
+ * A component meant to contain settings related to an individual track
+ * 
+ * fileType expects a string specifying the filetype, e.g "haplotype", "read"
+ * 
+ * trackColorSettings expects an object in the form of 
+ * {mainPallete: string,
+ *  auxPallate: string,
+ *  colorReadsByMappingQuality: boolean}
+ * 
+ * The handleInputChange function expects to be passed a new trackColorSettings object
+ * 
+ * availableColors expects an array of colors, must correspond to valid colors defined in RadioRow.js and tubemap.js
+ *  
+ * See demo and test file for examples of this component.
+ */
+
+export const TrackSettings = ({
+    fileType,
+    trackColorSettings,
+    setTrackColorSetting,
+    availableColors
+}) => {
+    function colorRenderSwitch(fileType) {
+        switch(fileType) {
+            case "graph":
+            case "haplotype":
+                return(
+                    <Form>
+                        <RadioRow
+                            rowHeading="Haplotypes Forward"
+                            color={trackColorSettings.mainPallete}
+                            trackType="mainPallete"
+                            setColorSetting={setTrackColorSetting}
+                            availableColors={availableColors}
+                        />
+                    </Form>
+                );
+            case  "read":
+                return(
+                    <Form>
+                        <RadioRow
+                            rowHeading="Gam Forward"
+                            color={trackColorSettings.mainPallete}
+                            trackType="mainPallete"
+                            setColorSetting={setTrackColorSetting}
+                            availableColors={availableColors}
+                        />
+                        <RadioRow
+                            rowHeading="Gam Reverse"
+                            color={trackColorSettings.auxPallete}
+                            trackType="auxPallete"
+                            setColorSetting={setTrackColorSetting}
+                            availableColors={availableColors}
+                        />
+                    </Form>
+                );
+            default:
+                throw new Error("Invalid file type");
+        }
+    }
+
+    return(
+        <Container>
+            <Card>
+                <CardBody>
+                    <h5>Colors</h5>
+                    {colorRenderSwitch(fileType)}
+                </CardBody>
+            </Card>
+        </Container>
+    )
+}
+
+TrackSettings.propTypes = {
+    fileType: PropTypes.string.isRequired,
+    trackColorSettings: PropTypes.object.isRequired,
+    setTrackColorSetting: PropTypes.func.isRequired,
+    availableColors: PropTypes.array
+}
+
+TrackSettings.defaultProps = {
+    fileType: "haplotype",
+    availableColors: ["greys", "ygreys", "blues", "reds", "plainColors", "lightColors"]
+}
+
+export default TrackSettings;
\ No newline at end of file
diff --git a/src/components/TrackSettingsButton.demo.js b/src/components/TrackSettingsButton.demo.js
new file mode 100644
index 0000000..58c3ce9
--- /dev/null
+++ b/src/components/TrackSettingsButton.demo.js
@@ -0,0 +1,27 @@
+import Demo, {props as P} from 'react-demo'
+// See https://github.com/rpominov/react-demo for how to make a demo
+
+import TrackSettingsButton from "./TrackSettingsButton.js";
+
+export default (<Demo
+    props={{
+      fileType: P.choices(["haplotype", "graph", "read"]),
+      trackColorSettings: P.json({    
+        mainPallete: "blues",
+        auxPallete: "reds",
+        colorReadsByMappingQuality: false}),
+      availableColors: P.json(["greys", "ygreys", "blues", "reds", "plainColors"])
+    }}
+  >
+    {
+      (props, update) => {
+        return <TrackSettingsButton {...props} setTrackColorSetting={(key, value) => {
+          // modify a key in TrackColorSettings
+          let newTrackColorSettings = {...props.trackColorSettings};
+          newTrackColorSettings[key] = value;
+          update({trackColorSettings: newTrackColorSettings})
+        }}/>
+      }
+    }
+  </Demo>)
+  
diff --git a/src/components/TrackSettingsButton.js b/src/components/TrackSettingsButton.js
new file mode 100644
index 0000000..6c80262
--- /dev/null
+++ b/src/components/TrackSettingsButton.js
@@ -0,0 +1,30 @@
+import React from 'react';
+import Popup from 'reactjs-popup';
+import PropTypes from "prop-types";
+import TrackSettings from "./TrackSettings.js";
+
+export const TrackSettingsButton = ({
+    fileType,
+    trackColorSettings,
+    setTrackColorSetting,
+    availableColors
+}) => {
+    return(
+        <Popup trigger={<button> Settings </button>} position="right center"> 
+            <TrackSettings 
+            fileType={fileType}
+            trackColorSettings={trackColorSettings}
+            availableColors={availableColors}
+            setTrackColorSetting={setTrackColorSetting}/>
+        </Popup>
+    )
+}
+
+TrackSettingsButton.propTypes = {
+    fileType: PropTypes.string.isRequired,
+    trackColorSettings: PropTypes.object.isRequired,
+    setTrackColorSetting: PropTypes.func.isRequired,
+    availableColors: PropTypes.array
+}
+
+export default TrackSettingsButton;
\ No newline at end of file


Test patch:
diff --git a/src/components/TrackSettings.test.js b/src/components/TrackSettings.test.js
new file mode 100644
index 0000000..18651b0
--- /dev/null
+++ b/src/components/TrackSettings.test.js
@@ -0,0 +1,67 @@
+import React from 'react';
+import { render, fireEvent}  from '@testing-library/react';
+import {TrackSettings} from './TrackSettings';
+import '@testing-library/jest-dom'
+
+describe('TrackSettings', () => {
+    const availableColors = ["greys", "ygreys", "blues", "reds", "plainColors", "lightColors"];
+    const trackColorSettings = {    
+        mainPallete: "blues",
+        auxPallete: "reds",
+        colorReadsByMappingQuality: false};
+
+    it('should render without errors', async () => {
+        const fakeOnChange = jest.fn();
+        const { getByText } = render(
+            <TrackSettings 
+            fileType={"haplotype"}
+            trackColorSettings={trackColorSettings}
+            availableColors={availableColors}
+            setTrackColorSetting={fakeOnChange}/>
+        );
+
+        // maybe need to search by "Select a file"
+        const placeholder = await getByText("blues");
+        expect(placeholder).toBeTruthy();
+    });
+
+    it('should call onChange when an option is selected', async () => {
+        const fakeOnChange = jest.fn();
+        const { getByLabelText } = render(
+            <TrackSettings 
+            fileType={"haplotype"}
+            trackColorSettings={trackColorSettings}
+            availableColors={availableColors}
+            setTrackColorSetting={fakeOnChange}/>
+        );
+
+        const redLabel = getByLabelText('reds');
+        const greyLabel = getByLabelText('greyscale');
+
+        expect(fakeOnChange).toHaveBeenCalledTimes(0);    
+
+        fireEvent.click(redLabel);
+        expect(fakeOnChange).toHaveBeenCalledTimes(1);
+
+        fireEvent.click(greyLabel);
+        expect(fakeOnChange).toHaveBeenCalledTimes(2);
+        expect(fakeOnChange).toHaveBeenCalledWith("mainPallete", "greys");
+    });
+
+    it('should update the radio values when an option is selected', async () => {
+        const fakeOnChange = jest.fn();
+        const { getByLabelText } = render(
+            <TrackSettings 
+            fileType={"haplotype"}
+            trackColorSettings={trackColorSettings}
+            availableColors={availableColors}
+            setTrackColorSetting={fakeOnChange}/>
+        );
+
+        const radio = getByLabelText("blues")
+        fireEvent.change(radio, {target: { value: "reds" }});
+        expect(radio.value).toBe("reds");
+    })
+
+
+});
\ No newline at end of file