-
Notifications
You must be signed in to change notification settings - Fork 166
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Color tree by measurements #1924
Changes from 26 commits
d90c503
6c2e53b
15dabdf
14a5650
1391b9b
acf11a4
a4b4d9c
69853e2
8b2773e
74eac4a
c0b7e91
e1c74a4
9a5a183
3c35ed5
c2db2bb
17d411c
ade2c20
440cddd
ff28517
795efc7
9a4c328
5d51e9c
0dbc4e3
fb3eba0
a2cfe54
bb44c2a
82e6f6d
9f6b032
27cdbb0
e52f48d
972ed91
e586a6c
921161c
aead1cf
daccd78
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,5 +1,5 @@ | ||
import React, { CSSProperties, MutableRefObject, useCallback, useRef, useEffect, useMemo, useState } from "react"; | ||
import { useSelector } from "react-redux"; | ||
import { useDispatch, useSelector } from "react-redux"; | ||
import { isEqual, orderBy } from "lodash"; | ||
import { NODE_VISIBLE } from "../../util/globals"; | ||
import { getColorByTitle, getTipColorAttribute } from "../../util/colorHelpers"; | ||
|
@@ -23,12 +23,20 @@ import { | |
addHoverPanelToMeasurementsAndMeans, | ||
addColorByAttrToGroupingLabel, | ||
layout, | ||
jitterRawMeansByColorBy | ||
jitterRawMeansByColorBy, | ||
addGroupingValueCrosshair, | ||
removeColorGroupingCrosshair, | ||
} from "./measurementsD3"; | ||
import { RootState } from "../../store"; | ||
import { MeasurementFilters } from "../../reducers/controls"; | ||
import { Visibility } from "../../reducers/tree/types"; | ||
import { Measurement, isMeasurement } from "../../reducers/measurements/types"; | ||
import { | ||
applyMeasurementsColorBy, | ||
isMeasurementColorBy, | ||
getActiveMeasurementFilters, | ||
matchesAllActiveFilters | ||
} from "../../actions/measurements"; | ||
|
||
interface MeanAndStandardDeviation { | ||
mean: number | ||
|
@@ -131,39 +139,28 @@ const filterMeasurements = ( | |
filteredMeasurements: Measurement[] | ||
} => { | ||
// Find active filters to filter measurements | ||
const activeFilters: {string?: string[]} = {}; | ||
Object.entries(filters).forEach(([field, valuesMap]) => { | ||
activeFilters[field] = activeFilters[field] || []; | ||
valuesMap.forEach(({active}, fieldValue) => { | ||
// Save array of active values for the field filter | ||
if (active) activeFilters[field].push(fieldValue); | ||
}); | ||
}); | ||
const activeFilters = getActiveMeasurementFilters(filters); | ||
|
||
return { | ||
activeFilters, | ||
filteredMeasurements: measurements.filter((measurement) => { | ||
// First check the strain is visible in the tree | ||
if (!isVisible(treeStrainVisibility[measurement.strain])) return false; | ||
// Then check that the measurement contains values for all active filters | ||
for (const [field, values] of Object.entries(activeFilters)) { | ||
const measurementValue = measurement[field]; | ||
if (values.length > 0 && | ||
((typeof measurementValue === "string") && !values.includes(measurementValue))){ | ||
return false; | ||
} | ||
} | ||
return true; | ||
return matchesAllActiveFilters(measurement, activeFilters); | ||
}) | ||
}; | ||
}; | ||
|
||
const MeasurementsPlot = ({height, width, showLegend, setPanelTitle}) => { | ||
const dispatch = useDispatch(); | ||
// Use `lodash.isEqual` to deep compare object states to prevent unnecessary re-renderings of the component | ||
const { treeStrainVisibility, treeStrainColors } = useSelector((state: RootState) => treeStrainPropertySelector(state), isEqual); | ||
const legendValues = useSelector((state: RootState) => state.controls.colorScale.legendValues, isEqual); | ||
// Convert legendValues to string to ensure that subsequent attribute matches work as intended | ||
const legendValues = useSelector((state: RootState) => state.controls.colorScale.legendValues.map((v): string => v.toString()), isEqual); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. A few minor points - don't change them if you don't agree with them:
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah good point about
These are referring to the coloring attributes, so yes these correspond to the node attributes / node values.
These are referring to where the node attributes get compared to the legend values for ordering. These are in measurementsD3.js, within jitterRawMeansByColorBy and drawMeansForColorBy. |
||
const colorings = useSelector((state: RootState) => state.metadata.colorings); | ||
const colorBy = useSelector((state: RootState) => state.controls.colorBy); | ||
joverlee521 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
const colorGrouping = useSelector((state: RootState) => state.controls.measurementsColorGrouping); | ||
const groupBy = useSelector((state: RootState) => state.controls.measurementsGroupBy); | ||
const filters = useSelector((state: RootState) => state.controls.measurementsFilters); | ||
const display = useSelector((state: RootState) => state.controls.measurementsDisplay); | ||
|
@@ -257,6 +254,12 @@ const MeasurementsPlot = ({height, width, showLegend, setPanelTitle}) => { | |
setHoverData(newHoverData); | ||
}, [fields, colorings, colorBy]); | ||
|
||
const handleClickOnGrouping = useCallback((grouping: string): void => { | ||
if (grouping !== colorGrouping || !isMeasurementColorBy(colorBy)) { | ||
dispatch(applyMeasurementsColorBy(grouping)); | ||
} | ||
}, [dispatch, colorGrouping, colorBy]); | ||
|
||
useEffect(() => { | ||
setPanelTitle(`${title || "Measurements"} (grouped by ${fields.get(groupBy).title})`); | ||
}, [setPanelTitle, title, fields, groupBy]); | ||
|
@@ -267,8 +270,8 @@ const MeasurementsPlot = ({height, width, showLegend, setPanelTitle}) => { | |
// the scroll position on whitespace. | ||
svgContainerRef.current.scrollTop = 0; | ||
clearMeasurementsSVG(d3Ref.current, d3XAxisRef.current); | ||
drawMeasurementsSVG(d3Ref.current, d3XAxisRef.current, svgData); | ||
}, [svgData]); | ||
drawMeasurementsSVG(d3Ref.current, d3XAxisRef.current, svgData, handleClickOnGrouping); | ||
}, [svgData, handleClickOnGrouping]); | ||
|
||
// Color the SVG & redraw color-by means when SVG is re-drawn or when colors have changed | ||
useEffect(() => { | ||
|
@@ -292,6 +295,14 @@ const MeasurementsPlot = ({height, width, showLegend, setPanelTitle}) => { | |
toggleDisplay(d3Ref.current, "threshold", showThreshold); | ||
}, [svgData, showThreshold]); | ||
|
||
useEffect(() => { | ||
if(isMeasurementColorBy(colorBy)) { | ||
addGroupingValueCrosshair(d3Ref.current, colorGrouping); | ||
} else { | ||
removeColorGroupingCrosshair(d3Ref.current); | ||
} | ||
}, [svgData, colorBy, colorGrouping]) | ||
|
||
const getSVGContainerStyle = (): CSSProperties => { | ||
return { | ||
overflowY: "auto", | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -39,6 +39,7 @@ layout['overallMeanYValue'] = layout.subplotHeight / 2; | |
const classes = { | ||
xAxis: "measurementXAxis", | ||
yAxis: "measurementYAxis", | ||
groupingValue: "measurementGroupingValue", | ||
yAxisColorByLabel: "measurementYAxisColorByLabel", | ||
threshold: "measurementThreshold", | ||
subplot: "measurementSubplot", | ||
|
@@ -189,7 +190,7 @@ const drawStickyXAxis = (ref, containerHeight, svgHeight, xScale, x_axis_label) | |
.text(x_axis_label); | ||
}; | ||
|
||
export const drawMeasurementsSVG = (ref, xAxisRef, svgData) => { | ||
export const drawMeasurementsSVG = (ref, xAxisRef, svgData, handleClickOnGrouping) => { | ||
const {containerHeight, xScale, yScale, x_axis_label, thresholds, groupingOrderedValues, groupedMeasurements} = svgData; | ||
|
||
// Do not draw SVG if there are no measurements | ||
|
@@ -251,6 +252,7 @@ export const drawMeasurementsSVG = (ref, xAxisRef, svgData) => { | |
subplot.append("g") | ||
.attr("class", classes.yAxis) | ||
.attr("transform", `translate(${layout.leftPadding}, 0)`) | ||
.attr("cursor", "pointer") | ||
.call( | ||
axisLeft(yScale) | ||
.tickValues([yScale((layout.yMax - layout.yMin) / 2)]) | ||
|
@@ -264,6 +266,7 @@ export const drawMeasurementsSVG = (ref, xAxisRef, svgData) => { | |
// but there're always limits of the available space so punting that for now. | ||
// -Jover, 20 September 2022 | ||
g.selectAll('text') | ||
.attr("class", classes.groupingValue) | ||
joverlee521 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
.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 | ||
|
@@ -273,7 +276,8 @@ export const drawMeasurementsSVG = (ref, xAxisRef, svgData) => { | |
} | ||
return null; | ||
}); | ||
}); | ||
}) | ||
.on("click", () => handleClickOnGrouping(groupingValue)); | ||
|
||
// Add circles for each measurement | ||
// Note, "cy" is added later when jittering within color-by groups | ||
|
@@ -466,7 +470,7 @@ export const addColorByAttrToGroupingLabel = (ref, treeStrainColors) => { | |
svg.selectAll(`.${classes.yAxis}`).select(".tick") | ||
.each((_, i, elements) => { | ||
const groupingLabel = select(elements[i]); | ||
const groupingValue = groupingLabel.text(); | ||
const groupingValue = groupingLabel.select(`.${classes.groupingValue}`).text(); | ||
const groupingValueColorBy = treeStrainColors[groupingValue]; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Yes, this is reflecting the color of the strain in the tree |
||
if (groupingValueColorBy) { | ||
// Get the current label width to add colored line and text relative to the width | ||
|
@@ -490,3 +494,38 @@ export const addColorByAttrToGroupingLabel = (ref, treeStrainColors) => { | |
} | ||
}); | ||
}; | ||
|
||
const colorGroupingCrosshairId = "measurementsColorGroupingCrosshair"; | ||
export const removeColorGroupingCrosshair = (ref) => { | ||
const svg = select(ref); | ||
svg.select(`#${colorGroupingCrosshairId}`).remove(); | ||
}; | ||
|
||
export const addGroupingValueCrosshair = (ref, groupingValue) => { | ||
// Remove previous color grouping crosshair | ||
removeColorGroupingCrosshair(ref); | ||
|
||
const svg = select(ref); | ||
svg.selectAll(`.${classes.yAxis}`).select(".tick") | ||
.each((_, i, elements) => { | ||
const groupingLabel = select(elements[i]); | ||
const currentGroupingValue = groupingLabel.select(`.${classes.groupingValue}`).text() | ||
if (groupingValue === currentGroupingValue){ | ||
const {width} = groupingLabel.node().getBoundingClientRect(); | ||
groupingLabel.append("svg") | ||
.attr("id", colorGroupingCrosshairId) | ||
.attr("stroke", "currentColor") | ||
.attr("fill", "currentColor") | ||
.attr("strokeWidth", "0") | ||
.attr("viewBox", "0 0 256 256") | ||
.attr("height", layout.yAxisColorByLineHeight * 2) | ||
.attr("width", layout.yAxisColorByLineHeight * 2) | ||
.attr("x", -width - (layout.yAxisColorByLineHeight * 2)) | ||
.attr("y", -layout.yAxisColorByLineHeight) | ||
joverlee521 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
.append("path") | ||
// path copied from react-icons/pi/PiCrosshairSimpleBold | ||
.attr("d", "M128,20A108,108,0,1,0,236,128,108.12,108.12,0,0,0,128,20Zm12,191.13V184a12,12,0,0,0-24,0v27.13A84.18,84.18,0,0,1,44.87,140H72a12,12,0,0,0,0-24H44.87A84.18,84.18,0,0,1,116,44.87V72a12,12,0,0,0,24,0V44.87A84.18,84.18,0,0,1,211.13,116H184a12,12,0,0,0,0,24h27.13A84.18,84.18,0,0,1,140,211.13Z" ) | ||
|
||
} | ||
}); | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we have two trees + measurements? I've come to the opinion that we should drop a lot of functionality when viewing two trees, so that if the second tree is displayed we don't allow measurements, frequencies, map to be displayed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Two trees + measurements is definitely possible right now, where the measurements panel always uses the measurements JSON from the main tree.
I'm not entirely sure how useful it is though...could it be useful for HA:NA + measurements?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can't answer that, but we should find the answer. Two trees complicates Auspice quite a lot, and I think it would be a worthwhile push to limit the cases in order to simplify the code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can revisit simplifying code in the future, starting conversation in Slack.