diff --git a/CHANGELOG.md b/CHANGELOG.md index a97e81eb1..b984736e0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,8 @@ * Fix bug where app crashed if measurements JSON did not define thresholds ([#1802](https://github.com/nextstrain/auspice/pull/1802)) * Fix bug where measurements display did not honor the default `measurements_display` ([#1802](https://github.com/nextstrain/auspice/pull/1802)) * Only display download-JSON button if the dataset name can be parsed from pathname ([#1804](https://github.com/nextstrain/auspice/pull/1804)) +* Fix bug where measurements panel did not display means for measurements that had an "undefined" coloring ([#1827](https://github.com/nextstrain/auspice/pull/1827)) +* Measurement panel's x-axis min/max values are now limited by visible measurements ([#1827](https://github.com/nextstrain/auspice/pull/1827)) ## version 2.56.0 - 2024/07/01 diff --git a/src/components/measurements/index.js b/src/components/measurements/index.js index 60f59261e..e10090066 100644 --- a/src/components/measurements/index.js +++ b/src/components/measurements/index.js @@ -1,4 +1,4 @@ -import React, { useRef, useEffect, useMemo, useState } from "react"; +import React, { useCallback, useRef, useEffect, useState } from "react"; import { useSelector } from "react-redux"; import { isEqual, orderBy } from "lodash"; import { NODE_VISIBLE } from "../../util/globals"; @@ -41,6 +41,17 @@ const useDeepCompareMemo = (value) => { return ref.current; }; +/** + * A wrapper around React's useCallback hook that does a deep comparison of the + * dependencies. + * @param {function} fn + * @param {Array} dependencies + * @returns + */ +const useDeepCompareCallback = (fn, dependencies) => { + return useCallback(fn, dependencies.map(useDeepCompareMemo)) +} + // Checks visibility against global NODE_VISIBLE const isVisible = (visibility) => visibility === NODE_VISIBLE; @@ -159,9 +170,9 @@ const MeasurementsPlot = ({height, width, showLegend, setPanelTitle}) => { } const groupedMeasurements = groupMeasurements(filteredMeasurements, groupBy, groupByValueOrder); - // Memoize D3 scale functions to allow deep comparison to work below for svgData - const xScale = useMemo(() => createXScale(width, measurements), [width, measurements]); - const yScale = useMemo(() => createYScale(), []); + // Cache D3 scale functions to allow deep comparison to work below for svgData + const xScale = useDeepCompareCallback(createXScale(width, filteredMeasurements), [width, filteredMeasurements]); + const yScale = useCallback(createYScale(), []); // Memoize all data needed for basic SVG to avoid extra re-drawings const svgData = useDeepCompareMemo({ containerHeight: height, @@ -172,8 +183,8 @@ const MeasurementsPlot = ({height, width, showLegend, setPanelTitle}) => { groupingOrderedValues, groupedMeasurements }); - // Memoize handleHover function to avoid extra useEffect calls - const handleHover = useMemo(() => (data, dataType, mouseX, mouseY, colorByAttr=null) => { + // Cache handleHover function to avoid extra useEffect calls + const handleHover = useCallback((data, dataType, mouseX, mouseY, colorByAttr=null) => { let newHoverData = null; if (data !== null) { // Set color-by attribute as title if provided diff --git a/src/components/measurements/measurementsD3.js b/src/components/measurements/measurementsD3.js index a387e6fad..fe69e0be4 100644 --- a/src/components/measurements/measurementsD3.js +++ b/src/components/measurements/measurementsD3.js @@ -58,15 +58,24 @@ const getSubplotDOMId = (groupingValueIndex) => `measurement_subplot_${groupingV /** * Creates the D3 linear scale for the x-axis with the provided measurements' * values as the domain and the panelWidth with hard-coded padding values as - * the range. Expected to be shared across all subplots. + * the range. The optional paddingProportion can be provided to include additional + * padding for the domain. Expected to be shared across all subplots. * @param {number} panelWidth * @param {Array} measurements + * @param {number} [paddingProportion=0.1] * @returns {function} */ -export const createXScale = (panelWidth, measurements) => { +export const createXScale = (panelWidth, measurements, paddingProportion = 0.1) => { + // Padding the xScale based on proportion + // Copied from https://github.com/d3/d3-scale/issues/150#issuecomment-561304239 + function padLinear([x0, x1], k) { + const dx = (x1 - x0) * k / 2; + return [x0 - dx, x1 + dx]; + } + return ( scaleLinear() - .domain(extent(measurements, (m) => m.value)) + .domain(padLinear(extent(measurements, (m) => m.value), paddingProportion)) .range([layout.leftPadding, panelWidth - layout.rightPadding]) .nice() ); @@ -377,8 +386,13 @@ export const drawMeansForColorBy = (ref, svgData, treeStrainColors, legendValues const ySpacing = (layout.subplotHeight - 4 * layout.subplotPadding) / (numberOfColorByAttributes - 1); let yValue = layout.subplotPadding; // Order the color groups by the legend value order so that we have a stable order for the means - legendValues - .filter((attribute) => String(attribute) in colorByGroups) + const orderedColorGroups = orderBy( + Object.keys(colorByGroups), + (key) => legendValues.indexOf(key), + "asc" + ); + + orderedColorGroups .forEach((attribute) => { const {color, values} = colorByGroups[attribute]; drawMeanAndStandardDeviation(