@@ -33,6 +33,10 @@ import DataClient, {
3333 VolcanoPlotRequestParams ,
3434 VolcanoPlotResponse ,
3535} from '../../../api/DataClient' ;
36+ import {
37+ VolcanoPlotData ,
38+ VolcanoPlotDataPoint ,
39+ } from '@veupathdb/components/lib/types/plots/volcanoplot' ;
3640import VolcanoSVG from './selectorIcons/VolcanoSVG' ;
3741import { NumberOrDate } from '@veupathdb/components/lib/types/general' ;
3842import { DifferentialAbundanceConfig } from '../../computations/plugins/differentialabundance' ;
@@ -239,14 +243,12 @@ function VolcanoPlotViz(props: VisualizationProps<Options>) {
239243 vizConfig . log2FoldChangeThreshold ?? DEFAULT_FC_THRESHOLD ;
240244
241245 /**
242- * Let's filter out data that falls outside of the plot axis ranges and then
243- * assign a significance color to the visible data
244246 * This version of the data will get passed to the VolcanoPlot component
245247 */
246248 const finalData = useMemo ( ( ) => {
247249 if ( data . value && independentAxisRange && dependentAxisRange ) {
248- // Only return data if the points fall within the specified range! Otherwise they'll show up on the plot.
249- return data . value
250+ const cleanedData = data . value
251+ // Only return data if the points fall within the specified range! Otherwise they'll show up on the plot.
250252 . filter ( ( d ) => {
251253 const log2foldChange = Number ( d ?. log2foldChange ) ;
252254 const transformedPValue = - Math . log10 ( Number ( d ?. pValue ) ) ;
@@ -257,16 +259,64 @@ function VolcanoPlotViz(props: VisualizationProps<Options>) {
257259 transformedPValue >= dependentAxisRange . min
258260 ) ;
259261 } )
260- . map ( ( d ) => ( {
261- ...d ,
262- significanceColor : assignSignificanceColor (
263- Number ( d . log2foldChange ) ,
264- Number ( d . pValue ) ,
265- significanceThreshold ,
266- log2FoldChangeThreshold ,
267- significanceColors
268- ) ,
269- } ) ) ;
262+ /**
263+ * Okay, this map function is doing a number of things.
264+ * 1. We're going to remove the pointID property and replace it with a pointIDs property that is an array of strings.
265+ * Some data share coordinates but correspond to a different pointID. By converting pointID to pointIDs, we can
266+ * later aggregate data that share coordinates and then render one tooltip that lists all pointIDs corresponding
267+ * to the point on the plot
268+ * 2. We also add a significanceColor property that is assigned a value that gets used in VolcanoPlot when rendering
269+ * the data point and the data point's tooltip. The property is also used in the countsData logic.
270+ */
271+ . map ( ( d ) => {
272+ const { pointID, ...remainingProperties } = d ;
273+ return {
274+ ...remainingProperties ,
275+ pointIDs : pointID ? [ pointID ] : undefined ,
276+ significanceColor : assignSignificanceColor (
277+ Number ( d . log2foldChange ) ,
278+ Number ( d . pValue ) ,
279+ significanceThreshold ,
280+ log2FoldChangeThreshold ,
281+ significanceColors
282+ ) ,
283+ } ;
284+ } )
285+ // Sort data in ascending order for tooltips to work most effectively
286+ . sort ( ( a , b ) => Number ( a . log2foldChange ) - Number ( b . log2foldChange ) ) ;
287+
288+ // Here we're going to loop through the cleanedData to aggregate any data with shared coordinates.
289+ // For each entry, we'll check if our aggregatedData includes an item with the same coordinates:
290+ // Yes? => update the matched aggregatedData element's pointID array to include the pointID of the matching entry
291+ // No? => just push the entry onto the aggregatedData array since no match was found
292+ const aggregatedData : VolcanoPlotData = [ ] ;
293+ for ( const entry of cleanedData ) {
294+ const foundIndex = aggregatedData . findIndex (
295+ ( d : VolcanoPlotDataPoint ) =>
296+ d . log2foldChange === entry . log2foldChange &&
297+ d . pValue === entry . pValue
298+ ) ;
299+ if ( foundIndex === - 1 ) {
300+ aggregatedData . push ( entry ) ;
301+ } else {
302+ const { pointIDs } = aggregatedData [ foundIndex ] ;
303+ if ( pointIDs ) {
304+ aggregatedData [ foundIndex ] = {
305+ ...aggregatedData [ foundIndex ] ,
306+ pointIDs : [
307+ ...pointIDs ,
308+ ...( entry . pointIDs ? entry . pointIDs : [ ] ) ,
309+ ] ,
310+ } ;
311+ } else {
312+ aggregatedData [ foundIndex ] = {
313+ ...aggregatedData [ foundIndex ] ,
314+ pointIDs : entry . pointIDs ,
315+ } ;
316+ }
317+ }
318+ }
319+ return aggregatedData ;
270320 }
271321 } , [
272322 data . value ,
@@ -276,7 +326,7 @@ function VolcanoPlotViz(props: VisualizationProps<Options>) {
276326 log2FoldChangeThreshold ,
277327 ] ) ;
278328
279- // For the legend, we need the counts of each assigned significance value
329+ // For the legend, we need the counts of the data
280330 const countsData = useMemo ( ( ) => {
281331 if ( ! finalData ) return ;
282332 const counts = {
@@ -285,7 +335,14 @@ function VolcanoPlotViz(props: VisualizationProps<Options>) {
285335 [ significanceColors [ 'low' ] ] : 0 ,
286336 } ;
287337 for ( const entry of finalData ) {
288- counts [ entry . significanceColor ] ++ ;
338+ if ( entry . significanceColor ) {
339+ // Recall that finalData combines data with shared coords into one point in order to display a
340+ // single tooltip that lists all the pointIDs for that shared point. This means we need to use
341+ // the length of the pointID array to accurately reflect the counts of unique data (not unique coords).
342+ const addend = entry . pointIDs ?. length ?? 1 ;
343+ counts [ entry . significanceColor ] =
344+ addend + counts [ entry . significanceColor ] ;
345+ }
289346 }
290347 return counts ;
291348 } , [ finalData ] ) ;
@@ -294,7 +351,7 @@ function VolcanoPlotViz(props: VisualizationProps<Options>) {
294351 updateThumbnail ,
295352 plotContainerStyles ,
296353 [
297- data ,
354+ finalData ,
298355 // vizConfig.checkedLegendItems, TODO
299356 vizConfig . independentAxisRange ,
300357 vizConfig . dependentAxisRange ,
0 commit comments