From 2d3ff9ede6587585dd5dff74d16ff3aad3a05ae1 Mon Sep 17 00:00:00 2001 From: Jover Date: Tue, 13 Sep 2022 13:21:40 -0700 Subject: [PATCH 01/10] measurements: truncate long titles MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Previously, long titles will push the measurements panel into a separate line when viewing in "grid" mode¹. This commit updates the CSS for the title of the Measurements panel so that long titles are truncated with an ellipsis and do not interrupt the layout of the page. I had considered another option to wrap the long title instead of truncating it, but this pushes the panel slightly out of line with the tree panel. ¹ https://github.com/nextstrain/auspice/pull/1452#discussion_r814392097 --- src/components/measurements/index.js | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/components/measurements/index.js b/src/components/measurements/index.js index dd16160fc..ce3d28225 100644 --- a/src/components/measurements/index.js +++ b/src/components/measurements/index.js @@ -261,8 +261,23 @@ const Measurements = ({height, width, showLegend}) => { const [title, setTitle] = useState("Measurements"); + const getCardTitleStyle = () => { + /** + * Additional styles of Card title forces it to be in one line and display + * ellipsis if the title is too long to prevent the long title from pushing + * the Card into the next line when viewing in grid mode + */ + return { + width, + display: "block", + overflow: "hidden", + whiteSpace: "nowrap", + textOverflow: "ellipsis" + }; + }; + return ( - + {measurementsLoaded && (measurementsError ? From 64affe0b5658ef293deced5f003a24c9e2b0f8e0 Mon Sep 17 00:00:00 2001 From: Jover Date: Thu, 15 Sep 2022 17:00:51 -0700 Subject: [PATCH 02/10] InfoLine: allow 0 values for display This change is motivated by the measurements hover panel display of titer values that can be 0, but I think this is a good change in general because a 0 value should be a valid value for display. --- src/components/tree/infoPanels/hover.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/tree/infoPanels/hover.js b/src/components/tree/infoPanels/hover.js index c058f3f27..cbbd7772d 100644 --- a/src/components/tree/infoPanels/hover.js +++ b/src/components/tree/infoPanels/hover.js @@ -11,7 +11,7 @@ import { parseIntervalsOfNsOrGaps } from "./MutationTable"; export const InfoLine = ({name, value, padBelow=false}) => { const renderValues = () => { - if (!value) return null; + if (!value && value !== 0) return null; if (Array.isArray(value)) { return value.map((v) => (
From 88f2b1ce9b167efd4cbc19dd99b7f92170f0562d Mon Sep 17 00:00:00 2001 From: Jover Date: Tue, 13 Sep 2022 15:06:05 -0700 Subject: [PATCH 03/10] refactor: make legend title of colorBy a util func Extract the function to create the title of the legend from the selected colorBy as a util/colorHelpers function. This is done in anticipation that we want a similar title string for the hover panel of the measurements panel. --- src/components/tree/legend/legend.js | 17 +++-------------- src/util/colorHelpers.js | 17 +++++++++++++++++ 2 files changed, 20 insertions(+), 14 deletions(-) diff --git a/src/components/tree/legend/legend.js b/src/components/tree/legend/legend.js index 856280c45..2b4d14969 100644 --- a/src/components/tree/legend/legend.js +++ b/src/components/tree/legend/legend.js @@ -4,9 +4,8 @@ import { rgb } from "d3-color"; import LegendItem from "./item"; import { headerFont, darkGrey } from "../../../globalStyles"; import { fastTransitionDuration, months } from "../../../util/globals"; -import { getBrighterColor } from "../../../util/colorHelpers"; +import { getBrighterColor, getColorByTitle } from "../../../util/colorHelpers"; import { numericToCalendar } from "../../../util/dateHelpers"; -import { isColorByGenotype, decodeColorByGenotype } from "../../../util/getGenotype"; import { TOGGLE_LEGEND } from "../../../actions/types"; const ITEM_RECT_SIZE = 15; @@ -61,22 +60,12 @@ class Legend extends React.Component { const rowPos = rowIdx * (ITEM_RECT_SIZE + LEGEND_SPACING); return `translate(${colPos},${rowPos})`; } - getTitleString() { - if (isColorByGenotype(this.props.colorBy)) { - const genotype = decodeColorByGenotype(this.props.colorBy); - return genotype.aa - ? `Genotype at ${genotype.gene} site ${genotype.positions.join(", ")}` - : `Nucleotide at position ${genotype.positions.join(", ")}`; - } - return this.props.colorings[this.props.colorBy] === undefined ? - "" : this.props.colorings[this.props.colorBy].title; - } getTitleWidth() { // This is a hack because we can't use getBBox in React. // Lots of work to get measured width of DOM element. // Works fine, but will need adjusting if title font is changed. - return 15 + 5.3 * this.getTitleString().length; + return 15 + 5.3 * getColorByTitle(this.props.colorings, this.props.colorBy).length; } toggleLegend() { @@ -101,7 +90,7 @@ class Legend extends React.Component { backgroundColor: "#fff" }} > - {this.getTitleString()} + {getColorByTitle(this.props.colorings, this.props.colorBy)} ); diff --git a/src/util/colorHelpers.js b/src/util/colorHelpers.js index bbf56726f..a81bc78eb 100644 --- a/src/util/colorHelpers.js +++ b/src/util/colorHelpers.js @@ -122,3 +122,20 @@ export const getEmphasizedColor = (color) => { }; export const getBrighterColor = (color) => rgb(color).brighter([0.65]).toString(); + +/** + * Return the display title for the selected colorBy + * @param {obj} colorings an object of available colorings + * @param {string} colorBy the select colorBy + * @returns {string} the display title for the colorBY + */ +export const getColorByTitle = (colorings, colorBy) => { + if (isColorByGenotype(colorBy)) { + const genotype = decodeColorByGenotype(colorBy); + return genotype.aa + ? `Genotype at ${genotype.gene} site ${genotype.positions.join(", ")}` + : `Nucleotide at position ${genotype.positions.join(", ")}`; + } + return colorings[colorBy] === undefined ? + "" : colorings[colorBy].title; +}; From ecdc78c7ff1d06f3465d78adf9dca39a94b3f31a Mon Sep 17 00:00:00 2001 From: Jover Date: Tue, 13 Sep 2022 18:26:35 -0700 Subject: [PATCH 04/10] measurements: Add title to hoverPanel component This change will allow us to add the color-by attribute as a title to the hover panels for the measurements. --- src/components/measurements/hoverPanel.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/components/measurements/hoverPanel.js b/src/components/measurements/hoverPanel.js index ce71daa4d..eafdbf93b 100644 --- a/src/components/measurements/hoverPanel.js +++ b/src/components/measurements/hoverPanel.js @@ -4,7 +4,7 @@ import { InfoLine } from "../tree/infoPanels/hover"; const HoverPanel = ({hoverData}) => { if (hoverData === null) return null; - const { mouseX, mouseY, containerId, data } = hoverData; + const { hoverTitle, mouseX, mouseY, containerId, data } = hoverData; const panelStyle = { position: "absolute", minWidth: 200, @@ -54,6 +54,9 @@ const HoverPanel = ({hoverData}) => { return (
+
+ {hoverTitle} +
{[...data.entries()].map(([field, value]) => { return ( From 2e58ef43cc0823982dd58ca4b22f3432f84d753b Mon Sep 17 00:00:00 2001 From: Jover Date: Wed, 14 Sep 2022 16:41:51 -0700 Subject: [PATCH 05/10] measurements: extract handleHover from SVG drawing This commit decouples the handling of hover panels in mouseover/mouseout events from the SVG drawing functions. This is done in anticipation that we will be adding the color-by attribute to the hover panel title. With the color-by attribute titles, the handleHover function will be dependent on color-by changes, which we do _not_ want to force SVG redrawing. --- src/components/measurements/index.js | 10 ++-- src/components/measurements/measurementsD3.js | 56 +++++++++---------- 2 files changed, 32 insertions(+), 34 deletions(-) diff --git a/src/components/measurements/index.js b/src/components/measurements/index.js index ce3d28225..4be60ea67 100644 --- a/src/components/measurements/index.js +++ b/src/components/measurements/index.js @@ -19,7 +19,8 @@ import { colorMeasurementsSVG, changeMeasurementsDisplay, svgContainerDOMId, - toggleDisplay + toggleDisplay, + addHoverPanelToMeasurementsAndMeans } from "./measurementsD3"; /** @@ -201,13 +202,14 @@ const MeasurementsPlot = ({height, width, showLegend, setPanelTitle}) => { // Draw SVG from scratch useEffect(() => { clearMeasurementsSVG(d3Ref.current); - drawMeasurementsSVG(d3Ref.current, svgData, handleHover); - }, [svgData, handleHover]); + drawMeasurementsSVG(d3Ref.current, svgData); + }, [svgData]); // Color the SVG & redraw color-by means when SVG is re-drawn or when colors have changed useEffect(() => { colorMeasurementsSVG(d3Ref.current, treeStrainColors); - drawMeansForColorBy(d3Ref.current, svgData, treeStrainColors, handleHover); + drawMeansForColorBy(d3Ref.current, svgData, treeStrainColors); + addHoverPanelToMeasurementsAndMeans(d3Ref.current, handleHover); }, [svgData, treeStrainColors, handleHover]); // Display raw/mean measurements when SVG is re-drawn, colors have changed, or display has changed diff --git a/src/components/measurements/measurementsD3.js b/src/components/measurements/measurementsD3.js index 3a16c6760..3c42aa5a1 100644 --- a/src/components/measurements/measurementsD3.js +++ b/src/components/measurements/measurementsD3.js @@ -105,7 +105,7 @@ export const clearMeasurementsSVG = (ref) => { .selectAll("*").remove(); }; -const drawMeanAndStandardDeviation = (values, d3ParentNode, containerClass, color, xScale, yValue, handleHover) => { +const drawMeanAndStandardDeviation = (values, d3ParentNode, containerClass, color, xScale, yValue) => { const meanAndStandardDeviation = { mean: mean(values), standardDeviation: deviation(values) @@ -122,13 +122,7 @@ const drawMeanAndStandardDeviation = (values, d3ParentNode, containerClass, colo .attr("class", classes.mean) .attr("transform", (d) => `translate(${xScale(d.mean)}, ${yValue})`) .attr("d", symbol().type(symbolDiamond).size(layout.diamondSize)) - .attr("fill", color) - .on("mouseover", (d) => { - // Get mouse position for HoverPanel - const { clientX, clientY } = d3event; - handleHover(d, "mean", clientX, clientY); - }) - .on("mouseout", () => handleHover(null)); + .attr("fill", color); if (meanAndStandardDeviation.standardDeviation !== undefined) { container.append("line") @@ -138,17 +132,11 @@ const drawMeanAndStandardDeviation = (values, d3ParentNode, containerClass, colo .attr("y1", yValue) .attr("y2", yValue) .attr("stroke-width", layout.standardDeviationStroke) - .attr("stroke", color) - .on("mouseover", (d) => { - // Get mouse position for HoverPanel - const { clientX, clientY } = d3event; - handleHover(d, "mean", clientX, clientY); - }) - .on("mouseout", () => handleHover(null)); + .attr("stroke", color); } }; -export const drawMeasurementsSVG = (ref, svgData, handleHover) => { +export const drawMeasurementsSVG = (ref, svgData) => { const {xScale, yScale, x_axis_label, threshold, groupingOrderedValues, groupedMeasurements} = svgData; // Do not draw SVG if there are no measurements @@ -238,22 +226,15 @@ export const drawMeasurementsSVG = (ref, svgData, handleHover) => { .attr("cx", (d) => xScale(d.value)) .attr("cy", (d) => yScale(d[measurementJitterSymbol])) .attr("r", layout.circleRadius) - .on("mouseover", (d, i, elements) => { + .on("mouseover.radius", (d, i, elements) => { select(elements[i]).transition() .duration("100") .attr("r", layout.circleHoverRadius); - - // Get mouse position for HoverPanel - const { clientX, clientY } = d3event; - // sets hover data state to trigger the hover panel display - handleHover(d, "measurement", clientX, clientY); }) - .on("mouseout", (_, i, elements) => { + .on("mouseout.radius", (_, i, elements) => { select(elements[i]).transition() .duration("200") .attr("r", layout.circleRadius); - // sets hover data state to null to hide the hover panel display - handleHover(null); }); // Draw overall mean and standard deviation for measurement values @@ -263,8 +244,7 @@ export const drawMeasurementsSVG = (ref, svgData, handleHover) => { classes.overallMean, layout.overallMeanColor, xScale, - layout.overallMeanYValue, - handleHover + layout.overallMeanYValue ); }); }; @@ -277,7 +257,7 @@ export const colorMeasurementsSVG = (ref, treeStrainColors) => { .style("fill", (d) => getBrighterColor(treeStrainColors[d.strain].color)); }; -export const drawMeansForColorBy = (ref, svgData, treeStrainColors, handleHover) => { +export const drawMeansForColorBy = (ref, svgData, treeStrainColors) => { const { xScale, groupingOrderedValues, groupedMeasurements } = svgData; const svg = select(ref); // Re move all current color by means @@ -309,8 +289,7 @@ export const drawMeansForColorBy = (ref, svgData, treeStrainColors, handleHover) classes.colorMean, color, xScale, - yValue, - handleHover + yValue ); // Increate yValue for next attribute mean yValue += ySpacing; @@ -342,3 +321,20 @@ export const toggleDisplay = (ref, elementClass, displayOn) => { .selectAll(`.${classes[elementClass]}`) .attr("display", displayAttr); }; + +export const addHoverPanelToMeasurementsAndMeans = (ref, handleHover) => { + const svg = select(ref); + svg.selectAll(`.${classes.rawMeasurements},.${classes.mean},.${classes.standardDeviation}`) + .on("mouseover.hoverPanel", (d, i, elements) => { + // Get mouse position for HoverPanel + const { clientX, clientY } = d3event; + + // Use class name to check data type + const className = elements[i].getAttribute("class"); + const dataType = className === classes.rawMeasurements ? "measurement" : "mean"; + + // sets hover data state to trigger the hover panel display + handleHover(d, dataType, clientX, clientY); + }) + .on("mouseout.hoverPanel", () => handleHover(null)); +}; From de7ccb7387c43d09a9e989bfca088b7d5817ec46 Mon Sep 17 00:00:00 2001 From: Jover Date: Wed, 14 Sep 2022 17:23:24 -0700 Subject: [PATCH 06/10] measurements: Add color-by attr as hover panel title For both raw measurements and the color-by means, we use the color-by attribute as the title of the hover panel. This will make it easier to parse what the color means for each element in cases where the legend contains too many colors. Note the overall means' hover panels do not have a title because they are not linked to the color-by attribute. --- src/components/measurements/index.js | 13 ++++++---- src/components/measurements/measurementsD3.js | 24 ++++++++++++------- 2 files changed, 25 insertions(+), 12 deletions(-) diff --git a/src/components/measurements/index.js b/src/components/measurements/index.js index 4be60ea67..3dc4c2ebe 100644 --- a/src/components/measurements/index.js +++ b/src/components/measurements/index.js @@ -2,7 +2,7 @@ import React, { useRef, useEffect, useMemo, useState } from "react"; import { useSelector } from "react-redux"; import { isEqual, orderBy } from "lodash"; import { NODE_VISIBLE } from "../../util/globals"; -import { getTipColorAttribute } from "../../util/colorHelpers"; +import { getColorByTitle, getTipColorAttribute } from "../../util/colorHelpers"; import { determineLegendMatch } from "../../util/tipRadiusHelpers"; import ErrorBoundary from "../../util/errorBoundry"; import Flex from "../framework/flex"; @@ -129,6 +129,8 @@ const filterMeasurements = (measurements, treeStrainVisibility, filters) => { const MeasurementsPlot = ({height, width, showLegend, setPanelTitle}) => { // Use `lodash.isEqual` to deep compare object states to prevent unnecessary re-renderings of the component const { treeStrainVisibility, treeStrainColors } = useSelector((state) => treeStrainPropertySelector(state), isEqual); + const colorings = useSelector((state) => state.metadata.colorings); + const colorBy = useSelector((state) => state.controls.colorBy); const groupBy = useSelector((state) => state.controls.measurementsGroupBy); const filters = useSelector((state) => state.controls.measurementsFilters); const display = useSelector((state) => state.controls.measurementsDisplay); @@ -160,9 +162,11 @@ const MeasurementsPlot = ({height, width, showLegend, setPanelTitle}) => { // Memoize all data needed for basic SVG to avoid extra re-drawings const svgData = useDeepCompareMemo({ xScale, yScale, x_axis_label, threshold, groupingOrderedValues, groupedMeasurements}); // Memoize handleHover function to avoid extra useEffect calls - const handleHover = useMemo(() => (data, dataType, mouseX, mouseY) => { + const handleHover = useMemo(() => (data, dataType, mouseX, mouseY, colorByAttr=null) => { let newHoverData = null; if (data !== null) { + // Set color-by attribute as title if provided + const hoverTitle = colorByAttr !== null ? `Color by ${getColorByTitle(colorings, colorBy)} : ${colorByAttr}` : null; // Create a Map of data to save order of fields const newData = new Map(); if (dataType === "measurement") { @@ -186,6 +190,7 @@ const MeasurementsPlot = ({height, width, showLegend, setPanelTitle}) => { Object.entries(data).forEach(([key, value]) => newData.set(key, value)); } newHoverData = { + hoverTitle, mouseX, mouseY, containerId: svgContainerDOMId, @@ -193,7 +198,7 @@ const MeasurementsPlot = ({height, width, showLegend, setPanelTitle}) => { }; } setHoverData(newHoverData); - }, [fields]); + }, [fields, colorings, colorBy]); useEffect(() => { setPanelTitle(`${title || "Measurements"} (grouped by ${fields.get(groupBy).title})`); @@ -209,7 +214,7 @@ const MeasurementsPlot = ({height, width, showLegend, setPanelTitle}) => { useEffect(() => { colorMeasurementsSVG(d3Ref.current, treeStrainColors); drawMeansForColorBy(d3Ref.current, svgData, treeStrainColors); - addHoverPanelToMeasurementsAndMeans(d3Ref.current, handleHover); + addHoverPanelToMeasurementsAndMeans(d3Ref.current, handleHover, treeStrainColors); }, [svgData, treeStrainColors, handleHover]); // Display raw/mean measurements when SVG is re-drawn, colors have changed, or display has changed diff --git a/src/components/measurements/measurementsD3.js b/src/components/measurements/measurementsD3.js index 3c42aa5a1..8c590efea 100644 --- a/src/components/measurements/measurementsD3.js +++ b/src/components/measurements/measurementsD3.js @@ -105,8 +105,9 @@ export const clearMeasurementsSVG = (ref) => { .selectAll("*").remove(); }; -const drawMeanAndStandardDeviation = (values, d3ParentNode, containerClass, color, xScale, yValue) => { +const drawMeanAndStandardDeviation = (values, d3ParentNode, containerClass, colorBy, xScale, yValue) => { const meanAndStandardDeviation = { + colorByAttr: colorBy.attribute, mean: mean(values), standardDeviation: deviation(values) }; @@ -122,7 +123,7 @@ const drawMeanAndStandardDeviation = (values, d3ParentNode, containerClass, colo .attr("class", classes.mean) .attr("transform", (d) => `translate(${xScale(d.mean)}, ${yValue})`) .attr("d", symbol().type(symbolDiamond).size(layout.diamondSize)) - .attr("fill", color); + .attr("fill", colorBy.color); if (meanAndStandardDeviation.standardDeviation !== undefined) { container.append("line") @@ -132,7 +133,7 @@ const drawMeanAndStandardDeviation = (values, d3ParentNode, containerClass, colo .attr("y1", yValue) .attr("y2", yValue) .attr("stroke-width", layout.standardDeviationStroke) - .attr("stroke", color); + .attr("stroke", colorBy.color); } }; @@ -242,7 +243,7 @@ export const drawMeasurementsSVG = (ref, svgData) => { measurements.map((d) => d.value), subplot, classes.overallMean, - layout.overallMeanColor, + {attribute: null, color: layout.overallMeanColor}, xScale, layout.overallMeanYValue ); @@ -282,12 +283,12 @@ export const drawMeansForColorBy = (ref, svgData, treeStrainColors) => { // 2 x subplotPadding for padding around the overall mean display const ySpacing = (layout.subplotHeight - 4 * layout.subplotPadding) / (numberOfColorByAttributes - 1); let yValue = layout.subplotPadding; - Object.values(colorByGroups).forEach(({color, values}) => { + Object.entries(colorByGroups).forEach(([attribute, {color, values}]) => { drawMeanAndStandardDeviation( values, subplot, classes.colorMean, - color, + {attribute, color}, xScale, yValue ); @@ -322,7 +323,7 @@ export const toggleDisplay = (ref, elementClass, displayOn) => { .attr("display", displayAttr); }; -export const addHoverPanelToMeasurementsAndMeans = (ref, handleHover) => { +export const addHoverPanelToMeasurementsAndMeans = (ref, handleHover, treeStrainColors) => { const svg = select(ref); svg.selectAll(`.${classes.rawMeasurements},.${classes.mean},.${classes.standardDeviation}`) .on("mouseover.hoverPanel", (d, i, elements) => { @@ -333,8 +334,15 @@ export const addHoverPanelToMeasurementsAndMeans = (ref, handleHover) => { const className = elements[i].getAttribute("class"); const dataType = className === classes.rawMeasurements ? "measurement" : "mean"; + // For the means, the bound data includes the color-by attribute + // For the measurements, we need to get the color-by attribute from treeStrainColors + let colorByAttr = d.colorByAttr; + if (dataType === "measurement") { + colorByAttr = treeStrainColors[d.strain]?.attribute || "undefined"; + } + // sets hover data state to trigger the hover panel display - handleHover(d, dataType, clientX, clientY); + handleHover(d, dataType, clientX, clientY, colorByAttr); }) .on("mouseout.hoverPanel", () => handleHover(null)); }; From d24ff59bef7190a6bc081c2f4281f1069115fb2e Mon Sep 17 00:00:00 2001 From: Jover Date: Tue, 20 Sep 2022 15:26:57 -0700 Subject: [PATCH 07/10] measurements: force y-axis labels to scale to fit In cases where the y-axis grouping label is too long, the text is scaled down to fit the available space. This does mean that if the text is extremely long, it can become unreadable. We can improve on this by manually splitting the text into parts that can fit on multiple lines, but there's always limits of the available space so punting that for now. --- src/components/measurements/measurementsD3.js | 25 ++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/src/components/measurements/measurementsD3.js b/src/components/measurements/measurementsD3.js index 8c590efea..b5e1310dd 100644 --- a/src/components/measurements/measurementsD3.js +++ b/src/components/measurements/measurementsD3.js @@ -26,7 +26,8 @@ export const layout = { subplotFillOpacity: "0.15", diamondSize: 25, standardDeviationStroke: 2, - overallMeanColor: "#000" + overallMeanColor: "#000", + yAxisTickSize: 6 }; // Display overall mean at the center of each subplot layout['overallMeanYValue'] = layout.subplotHeight / 2; @@ -211,8 +212,26 @@ export const drawMeasurementsSVG = (ref, svgData) => { .call( axisLeft(yScale) .tickValues([yScale((layout.yMax - layout.yMin) / 2)]) - .tickFormat(() => groupingValue)) - .call((g) => g.attr("font-family", null)); + .tickSize(layout.yAxisTickSize) + .tickFormat(groupingValue)) + .call((g) => { + g.attr("font-family", null); + // If necessary, scale down the text to fit in the available space for the y-Axis labels + // This does mean that if the text is extremely long, it can be unreadable. + // We can improve on this by manually splitting the text into parts that can fit on multiple lines, + // but there's always limits of the available space so punting that for now. + // -Jover, 20 September 2022 + g.selectAll('text') + .attr("transform", (_, i, element) => { + const textWidth = select(element[i]).node().getBoundingClientRect().width; + // Subtract the twice the y-axis tick size to give some padding around the text + const availableTextWidth = layout.leftPadding - (2 * layout.yAxisTickSize); + if (textWidth > availableTextWidth) { + return `scale(${availableTextWidth / textWidth})`; + } + return null; + }); + }); // Add circles for each measurement subplot.append("g") From 0f3adb1b769d95b6bb47aec30d1911b55249f340 Mon Sep 17 00:00:00 2001 From: Jover Date: Fri, 23 Sep 2022 15:03:24 -0700 Subject: [PATCH 08/10] measurements: Add color-by to y-axis labels When the grouping value for the y-axis are reference strains, try to find the color and attribute of the reference strain so that they can be displayed with the y-axis label. Allows for easier correlation between the reference and the displayed measurements. --- src/components/measurements/index.js | 4 +- src/components/measurements/measurementsD3.js | 38 ++++++++++++++++++- 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/src/components/measurements/index.js b/src/components/measurements/index.js index 3dc4c2ebe..75fd745e9 100644 --- a/src/components/measurements/index.js +++ b/src/components/measurements/index.js @@ -20,7 +20,8 @@ import { changeMeasurementsDisplay, svgContainerDOMId, toggleDisplay, - addHoverPanelToMeasurementsAndMeans + addHoverPanelToMeasurementsAndMeans, + addColorByAttrToGroupingLabel } from "./measurementsD3"; /** @@ -212,6 +213,7 @@ const MeasurementsPlot = ({height, width, showLegend, setPanelTitle}) => { // Color the SVG & redraw color-by means when SVG is re-drawn or when colors have changed useEffect(() => { + addColorByAttrToGroupingLabel(d3Ref.current, treeStrainColors); colorMeasurementsSVG(d3Ref.current, treeStrainColors); drawMeansForColorBy(d3Ref.current, svgData, treeStrainColors); addHoverPanelToMeasurementsAndMeans(d3Ref.current, handleHover, treeStrainColors); diff --git a/src/components/measurements/measurementsD3.js b/src/components/measurements/measurementsD3.js index b5e1310dd..4de242dbb 100644 --- a/src/components/measurements/measurementsD3.js +++ b/src/components/measurements/measurementsD3.js @@ -27,7 +27,9 @@ export const layout = { diamondSize: 25, standardDeviationStroke: 2, overallMeanColor: "#000", - yAxisTickSize: 6 + yAxisTickSize: 6, + yAxisColorByLineHeight: 7, + yAxisColorByLineStrokeWidth: 2 }; // Display overall mean at the center of each subplot layout['overallMeanYValue'] = layout.subplotHeight / 2; @@ -35,6 +37,7 @@ layout['overallMeanYValue'] = layout.subplotHeight / 2; const classes = { xAxis: "measurementXAxis", yAxis: "measurementYAxis", + yAxisColorByLabel: "measurementYAxisColorByLabel", threshold: "measurementThreshold", subplot: "measurementSubplot", subplotBackground: "measurementSubplotBackground", @@ -365,3 +368,36 @@ export const addHoverPanelToMeasurementsAndMeans = (ref, handleHover, treeStrain }) .on("mouseout.hoverPanel", () => handleHover(null)); }; + +export const addColorByAttrToGroupingLabel = (ref, treeStrainColors) => { + const svg = select(ref); + // Remove all previous color-by labels for the y-axis + svg.selectAll(`.${classes.yAxisColorByLabel}`).remove(); + // Loop through the y-axis labels to check if they have a corresponding color-by + svg.selectAll(`.${classes.yAxis}`).select(".tick") + .each((_, i, elements) => { + const groupingLabel = select(elements[i]); + const groupingValue = groupingLabel.text(); + const groupingValueColorBy = treeStrainColors[groupingValue]; + if (groupingValueColorBy) { + // Get the current label width to add colored line and text relative to the width + const labelWidth = groupingLabel.node().getBoundingClientRect().width; + groupingLabel.append("line") + .attr("class", classes.yAxisColorByLabel) + .attr("x1", -layout.yAxisTickSize) + .attr("x2", -labelWidth) + .attr("y1", layout.yAxisColorByLineHeight) + .attr("y2", layout.yAxisColorByLineHeight) + .attr("stroke-width", layout.yAxisColorByLineStrokeWidth) + .attr("stroke", groupingValueColorBy.color); + + groupingLabel.append("text") + .attr("class", classes.yAxisColorByLabel) + .attr("x", labelWidth * -0.5) + .attr("dy", layout.yAxisColorByLineHeight * 2 + layout.yAxisColorByLineStrokeWidth) + .attr("text-anchor", "middle") + .attr("fill", "currentColor") + .text(`(${groupingValueColorBy.attribute})`); + } + }); +}; From c120508676e4c67facc07bcc2be1df5c4ca187d6 Mon Sep 17 00:00:00 2001 From: Jover Date: Tue, 4 Oct 2022 15:25:30 -0700 Subject: [PATCH 09/10] measurements: increase line widths Create thicker lines for SD and the y-axis label colorings for easier viewing and hovering. --- src/components/measurements/measurementsD3.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/measurements/measurementsD3.js b/src/components/measurements/measurementsD3.js index 4de242dbb..d7243d0fe 100644 --- a/src/components/measurements/measurementsD3.js +++ b/src/components/measurements/measurementsD3.js @@ -24,12 +24,12 @@ export const layout = { thresholdStroke: "#DDD", subplotFill: "#adb1b3", subplotFillOpacity: "0.15", - diamondSize: 25, - standardDeviationStroke: 2, + diamondSize: 50, + standardDeviationStroke: 3, overallMeanColor: "#000", yAxisTickSize: 6, - yAxisColorByLineHeight: 7, - yAxisColorByLineStrokeWidth: 2 + yAxisColorByLineHeight: 9, + yAxisColorByLineStrokeWidth: 4 }; // Display overall mean at the center of each subplot layout['overallMeanYValue'] = layout.subplotHeight / 2; From 76e71b919bb74e05def5528a3cad6bff2a8c1d4e Mon Sep 17 00:00:00 2001 From: Jover Date: Tue, 4 Oct 2022 15:30:33 -0700 Subject: [PATCH 10/10] measurements: keep tree strain colors regardless of visibility We need the tree strain colors for all strains regardless of visibility so that we can display the colors for the reference strains in the y-axis label even when the strain is not visible in the tree. --- src/components/measurements/index.js | 33 +++++++++++++--------------- 1 file changed, 15 insertions(+), 18 deletions(-) diff --git a/src/components/measurements/index.js b/src/components/measurements/index.js index 75fd745e9..1c40b68e6 100644 --- a/src/components/measurements/index.js +++ b/src/components/measurements/index.js @@ -65,25 +65,22 @@ const treeStrainPropertySelector = (state) => { // Only store properties of terminal strain nodes if (!node.hasChildren) { treeStrainVisibility[node.name] = tree.visibility[index]; - // Only store colors for visible strains since only measurmeents - // for visible strains will be displayed - if (isVisible(tree.visibility[index])) { - /* - * If the color scale is continuous, we want to group by the legend value - * instead of the specific strain attribute in order to combine all values - * within the legend bounds into a single group. - */ - let attribute = getTipColorAttribute(node, colorScale); - if (colorScale.continuous) { - const matchingLegendValue = colorScale.visibleLegendValues - .find((legendValue) => determineLegendMatch(legendValue, node, colorScale)); - if (matchingLegendValue !== undefined) attribute = matchingLegendValue; - } - treeStrainColors[node.name] = { - attribute, - color: tree.nodeColors[index] - }; + /* + * If the color scale is continuous, we want to group by the legend value + * instead of the specific strain attribute in order to combine all values + * within the legend bounds into a single group. + */ + let attribute = getTipColorAttribute(node, colorScale); + if (colorScale.continuous) { + const matchingLegendValue = colorScale.visibleLegendValues + .find((legendValue) => determineLegendMatch(legendValue, node, colorScale)); + if (matchingLegendValue !== undefined) attribute = matchingLegendValue; } + treeStrainColors[node.name] = { + attribute, + color: tree.nodeColors[index] + }; + } return treeStrainProperty; }, intitialTreeStrainProperty);