From 92c3b49cf4444db687faf2df8cc6c3277f31b8cf Mon Sep 17 00:00:00 2001 From: Usama Ansari Date: Mon, 20 Nov 2023 13:25:16 +0100 Subject: [PATCH 1/2] feat: implement heatmap multiples --- src/vis/heatmap/Heatmap.tsx | 79 +++++++++++++++--------------- src/vis/heatmap/HeatmapGrid.tsx | 47 ++++++++++++------ src/vis/heatmap/utils.ts | 10 ++++ src/vis/legend/ColorLegend.tsx | 10 ++-- src/vis/legend/ColorLegendVert.tsx | 10 ++-- 5 files changed, 96 insertions(+), 60 deletions(-) diff --git a/src/vis/heatmap/Heatmap.tsx b/src/vis/heatmap/Heatmap.tsx index dafe044b1..a627b47cb 100644 --- a/src/vis/heatmap/Heatmap.tsx +++ b/src/vis/heatmap/Heatmap.tsx @@ -135,29 +135,29 @@ export function Heatmap({ : d3.extent(groupedVals, (d) => d.aggregateVal as number), ) : config?.numColorScaleType === ENumericalColorScaleType.DIVERGENT - ? d3 - .scaleSequential( - d3.piecewise( - d3.interpolateRgb.gamma(2.2), - [ - '#003367', - '#16518a', - '#2e72ae', - '#5093cd', - '#77b5ea', - '#aad7fd', - '#F1F3F5', - '#fac7a9', - '#f99761', - '#e06d3b', - '#c2451a', - '#99230d', - '#6f0000', - ].reverse(), - ), - ) - .domain(d3.extent(groupedVals, (d) => d.aggregateVal as number)) - : null; + ? d3 + .scaleSequential( + d3.piecewise( + d3.interpolateRgb.gamma(2.2), + [ + '#003367', + '#16518a', + '#2e72ae', + '#5093cd', + '#77b5ea', + '#aad7fd', + '#F1F3F5', + '#fac7a9', + '#f99761', + '#e06d3b', + '#c2451a', + '#99230d', + '#6f0000', + ].reverse(), + ), + ) + .domain(d3.extent(groupedVals, (d) => d.aggregateVal as number)) + : null; const extGroupedVals = groupedVals.map((gV) => ({ ...gV, @@ -228,6 +228,7 @@ export function Heatmap({ width={width - margin.left - margin.right} scale={colorScale} height={20} + canvasIdentifier={`${column1.info.id}-${column2.info.id}`} range={[...colorScale.domain()]} title={`${config.aggregateType} ${config.aggregateType === EAggregateTypes.COUNT ? '' : config.aggregateColumn.name}`} /> @@ -243,10 +244,10 @@ export function Heatmap({ config.ySortedBy === ESortTypes.CAT_ASC ? ESortTypes.CAT_DESC : config.ySortedBy === ESortTypes.CAT_DESC - ? ESortTypes.VAL_ASC - : config.ySortedBy === ESortTypes.VAL_ASC - ? ESortTypes.VAL_DESC - : ESortTypes.CAT_ASC, + ? ESortTypes.VAL_ASC + : config.ySortedBy === ESortTypes.VAL_ASC + ? ESortTypes.VAL_DESC + : ESortTypes.CAT_ASC, }) } > @@ -258,10 +259,10 @@ export function Heatmap({ config.ySortedBy === ESortTypes.VAL_ASC ? faArrowDownShortWide : config.ySortedBy === ESortTypes.VAL_DESC - ? faArrowDownWideShort - : config.ySortedBy === ESortTypes.CAT_ASC - ? faArrowDownAZ - : faArrowDownZA + ? faArrowDownWideShort + : config.ySortedBy === ESortTypes.CAT_ASC + ? faArrowDownAZ + : faArrowDownZA } /> {column2.info.name} @@ -294,10 +295,10 @@ export function Heatmap({ config.xSortedBy === ESortTypes.CAT_ASC ? ESortTypes.CAT_DESC : config.xSortedBy === ESortTypes.CAT_DESC - ? ESortTypes.VAL_ASC - : config.xSortedBy === ESortTypes.VAL_ASC - ? ESortTypes.VAL_DESC - : ESortTypes.CAT_ASC, + ? ESortTypes.VAL_ASC + : config.xSortedBy === ESortTypes.VAL_ASC + ? ESortTypes.VAL_DESC + : ESortTypes.CAT_ASC, }) } > @@ -308,10 +309,10 @@ export function Heatmap({ config.xSortedBy === ESortTypes.VAL_ASC ? faArrowDownShortWide : config.xSortedBy === ESortTypes.VAL_DESC - ? faArrowDownWideShort - : config.xSortedBy === ESortTypes.CAT_ASC - ? faArrowDownAZ - : faArrowDownZA + ? faArrowDownWideShort + : config.xSortedBy === ESortTypes.CAT_ASC + ? faArrowDownAZ + : faArrowDownZA } /> {column1.info.name} diff --git a/src/vis/heatmap/HeatmapGrid.tsx b/src/vis/heatmap/HeatmapGrid.tsx index 05af2e414..d482f06df 100644 --- a/src/vis/heatmap/HeatmapGrid.tsx +++ b/src/vis/heatmap/HeatmapGrid.tsx @@ -1,11 +1,11 @@ -import { Loader, Stack } from '@mantine/core'; -import * as React from 'react'; +import { Box, Loader, Stack } from '@mantine/core'; +import React, { useMemo } from 'react'; import { useAsync } from '../../hooks/useAsync'; import { InvalidCols } from '../general/InvalidCols'; import { VisColumn } from '../interfaces'; import { Heatmap } from './Heatmap'; import { IHeatmapConfig } from './interfaces'; -import { getHeatmapData } from './utils'; +import { getHeatmapData, setsOfTwo } from './utils'; export function HeatmapGrid({ config, @@ -21,9 +21,9 @@ export function HeatmapGrid({ selected?: { [key: string]: boolean }; }) { const { value: allColumns, status } = useAsync(getHeatmapData, [columns, config.catColumnsSelected, config.aggregateColumn]); - const hasAtLeast2CatCols = allColumns?.catColumn && allColumns?.catColumn?.length > 1; + const hasAtLeast2CatCols = useMemo(() => allColumns?.catColumn && allColumns?.catColumn?.length > 1, [allColumns?.catColumn]); - const margin = React.useMemo(() => { + const margin = useMemo(() => { return { top: 10, right: 20, @@ -32,6 +32,10 @@ export function HeatmapGrid({ }; }, []); + const heatmapMultiples = useMemo(() => { + return setsOfTwo(hasAtLeast2CatCols ? allColumns?.catColumn : []) as Awaited>['catColumn'][]; + }, [allColumns?.catColumn, hasAtLeast2CatCols]); + return ( {status === 'pending' ? ( @@ -39,16 +43,29 @@ export function HeatmapGrid({ ) : !hasAtLeast2CatCols ? ( ) : ( - + + {heatmapMultiples.map(([column1, column2]) => ( + + ))} + )} ); diff --git a/src/vis/heatmap/utils.ts b/src/vis/heatmap/utils.ts index 3109564a3..9a1ffcb4e 100644 --- a/src/vis/heatmap/utils.ts +++ b/src/vis/heatmap/utils.ts @@ -50,3 +50,13 @@ export async function getHeatmapData( return { catColumn, aggregateColumn }; } + +export const setsOfTwo = (arr: T[]) => { + const result: T[][] = []; + for (let i = 0; i < arr.length; i++) { + for (let j = i + 1; j < arr.length; j++) { + result.push([arr[i], arr[j]]); + } + } + return result; +}; diff --git a/src/vis/legend/ColorLegend.tsx b/src/vis/legend/ColorLegend.tsx index 0b238d6c0..fad3201f0 100644 --- a/src/vis/legend/ColorLegend.tsx +++ b/src/vis/legend/ColorLegend.tsx @@ -12,6 +12,7 @@ export function ColorLegend({ format = '.3s', rightMargin = 40, title = null, + canvasIdentifier = '', }: { scale: (t: number) => string; width?: number; @@ -21,6 +22,7 @@ export function ColorLegend({ format?: string; rightMargin?: number; title: string; + canvasIdentifier?: string; }) { const colors = d3 .range(tickCount) @@ -32,8 +34,10 @@ export function ColorLegend({ const canvasRef = useRef(null); + const canvasId = useMemo(() => `vertical-color-legend-canvas-${canvasIdentifier}`, [canvasIdentifier]); + useEffect(() => { - const canvas: HTMLCanvasElement = document.getElementById('proteomicsLegendCanvas') as HTMLCanvasElement; + const canvas: HTMLCanvasElement = document.getElementById(canvasId) as HTMLCanvasElement; const context = canvas.getContext('2d'); canvas.width = width; @@ -52,7 +56,7 @@ export function ColorLegend({ context.fillStyle = scale(t[i] + range[0]); context.fillRect(0, i, width, 1); } - }, [scale, width, height, range]); + }, [scale, width, height, range, canvasId]); const formatFunc = useMemo(() => { return d3.format(format); @@ -60,7 +64,7 @@ export function ColorLegend({ return ( - + {colors.map((color, i) => ( // idk why this doesnt work when i use the score as the key, tbh. The scores definitely are unique, but something to do with the 0 changing on render, idk diff --git a/src/vis/legend/ColorLegendVert.tsx b/src/vis/legend/ColorLegendVert.tsx index 3d4962358..fcec9312d 100644 --- a/src/vis/legend/ColorLegendVert.tsx +++ b/src/vis/legend/ColorLegendVert.tsx @@ -11,6 +11,7 @@ export function ColorLegendVert({ tickCount = 5, title = null, format = '.3s', + canvasIdentifier = '', }: { scale: (t: number) => string; width?: number; @@ -19,6 +20,7 @@ export function ColorLegendVert({ tickCount?: number; title: string; format?: string; + canvasIdentifier?: string; }) { const colors = d3 .range(tickCount) @@ -30,8 +32,10 @@ export function ColorLegendVert({ const canvasRef = useRef(null); + const canvasId = useMemo(() => `vertical-color-legend-canvas-${canvasIdentifier}`, [canvasIdentifier]); + useEffect(() => { - const canvas: HTMLCanvasElement = document.getElementById('proteomicsLegendCanvas') as HTMLCanvasElement; + const canvas: HTMLCanvasElement = document.getElementById(canvasId) as HTMLCanvasElement; const context = canvas.getContext('2d'); canvas.height = height; @@ -50,7 +54,7 @@ export function ColorLegendVert({ context.fillStyle = scale(t[i] + range[0]); context.fillRect(i, 0, 1, height); } - }, [scale, width, height, range]); + }, [scale, width, height, range, canvasId]); const formatFunc = useMemo(() => { return d3.format(format); @@ -63,7 +67,7 @@ export function ColorLegendVert({ {title} ) : null} - + {colors.map((color, i) => ( From c0ae43726e569b6a154d95335704ced1105ca0ba Mon Sep 17 00:00:00 2001 From: Usama Ansari Date: Mon, 20 Nov 2023 15:35:03 +0100 Subject: [PATCH 2/2] fix: restrict heatmaps to only 2 categorical columns --- src/vis/heatmap/HeatmapGrid.tsx | 78 +++++++++++-------- .../Vis/Heatmap/HeatmapRandom.stories.tsx | 77 +++++++++--------- 2 files changed, 87 insertions(+), 68 deletions(-) diff --git a/src/vis/heatmap/HeatmapGrid.tsx b/src/vis/heatmap/HeatmapGrid.tsx index d482f06df..80fc386b1 100644 --- a/src/vis/heatmap/HeatmapGrid.tsx +++ b/src/vis/heatmap/HeatmapGrid.tsx @@ -1,11 +1,11 @@ -import { Box, Loader, Stack } from '@mantine/core'; +import { /* Box, */ Loader, Stack } from '@mantine/core'; import React, { useMemo } from 'react'; import { useAsync } from '../../hooks/useAsync'; import { InvalidCols } from '../general/InvalidCols'; import { VisColumn } from '../interfaces'; import { Heatmap } from './Heatmap'; import { IHeatmapConfig } from './interfaces'; -import { getHeatmapData, setsOfTwo } from './utils'; +import { getHeatmapData /* , setsOfTwo */ } from './utils'; export function HeatmapGrid({ config, @@ -21,7 +21,10 @@ export function HeatmapGrid({ selected?: { [key: string]: boolean }; }) { const { value: allColumns, status } = useAsync(getHeatmapData, [columns, config.catColumnsSelected, config.aggregateColumn]); - const hasAtLeast2CatCols = useMemo(() => allColumns?.catColumn && allColumns?.catColumn?.length > 1, [allColumns?.catColumn]); + const hasTwoCatCols = useMemo(() => allColumns?.catColumn && allColumns?.catColumn?.length === 2, [allColumns?.catColumn]); + + // NOTE: @dv-usama-ansari: This flag is used when multiple heatmaps are rendered. + // const hasAtLeastTwoCatCols = useMemo(() => allColumns?.catColumn && allColumns?.catColumn?.length > 1, [allColumns?.catColumn]); const margin = useMemo(() => { return { @@ -32,40 +35,53 @@ export function HeatmapGrid({ }; }, []); - const heatmapMultiples = useMemo(() => { - return setsOfTwo(hasAtLeast2CatCols ? allColumns?.catColumn : []) as Awaited>['catColumn'][]; - }, [allColumns?.catColumn, hasAtLeast2CatCols]); + // NOTE: @dv-usama-ansari: This implementation for multiple heatmaps works, but it's not very performant. + // const heatmapMultiples = useMemo(() => { + // return setsOfTwo(hasTwoCatCols ? allColumns?.catColumn : []) as Awaited>['catColumn'][]; + // }, [allColumns?.catColumn, hasTwoCatCols]); return ( {status === 'pending' ? ( - ) : !hasAtLeast2CatCols ? ( - + ) : !hasTwoCatCols ? ( + ) : ( - - {heatmapMultiples.map(([column1, column2]) => ( - - ))} - + + + // NOTE: @dv-usama-ansari: This implementation for multiple heatmaps works, but it's not very performant. + // + // {heatmapMultiples.map(([column1, column2]) => ( + // + // ))} + // )} ); diff --git a/src/vis/stories/Vis/Heatmap/HeatmapRandom.stories.tsx b/src/vis/stories/Vis/Heatmap/HeatmapRandom.stories.tsx index 26d7dad43..d128d5b94 100644 --- a/src/vis/stories/Vis/Heatmap/HeatmapRandom.stories.tsx +++ b/src/vis/stories/Vis/Heatmap/HeatmapRandom.stories.tsx @@ -6,14 +6,16 @@ import { VisProvider } from '../../../Provider'; import { ESortTypes } from '../../../heatmap/interfaces'; import { BaseVisConfig, EAggregateTypes, EColumnTypes, ENumericalColorScaleType, ESupportedPlotlyVis, VisColumn } from '../../../interfaces'; -function RNG(seed) { - const m = 2 ** 35 - 31; - const a = 185852; - let s = seed % m; - return function () { - return (s = (s * a) % m) / m; - }; -} +// NOTE: @dv-usama-ansari: This function is not used anywhere, maybe it can be removed. +// function RNG(seed) { +// const m = 2 ** 35 - 31; +// const a = 185852; +// let s = seed % m; +// return function () { +// return (s = (s * a) % m) / m; +// }; +// } + function fetchData(numberOfPoints: number): VisColumn[] { const norm = d3.randomNormal.source(d3.randomLcg(0.5)); const rng = norm(0.5, 0.3); @@ -162,32 +164,33 @@ Basic.args = { } as BaseVisConfig, }; -export const Multiples: typeof Template = Template.bind({}) as typeof Template; -Multiples.args = { - externalConfig: { - type: ESupportedPlotlyVis.HEATMAP, - catColumnsSelected: [ - { - description: '', - id: 'category', - name: 'category', - }, - { - description: '', - id: 'category2', - name: 'category2', - }, - { - description: '', - id: 'category3', - name: 'category3', - }, - ], - xSortedBy: ESortTypes.CAT_ASC, - ySortedBy: ESortTypes.CAT_ASC, - color: null, - numColorScaleType: ENumericalColorScaleType.SEQUENTIAL, - aggregateColumn: null, - aggregateType: EAggregateTypes.COUNT, - } as BaseVisConfig, -}; +// NOTE: @dv-usama-ansari: This is the implementation for multiple heatmaps, but it's not very performant. +// export const Multiples: typeof Template = Template.bind({}) as typeof Template; +// Multiples.args = { +// externalConfig: { +// type: ESupportedPlotlyVis.HEATMAP, +// catColumnsSelected: [ +// { +// description: '', +// id: 'category', +// name: 'category', +// }, +// { +// description: '', +// id: 'category2', +// name: 'category2', +// }, +// { +// description: '', +// id: 'category3', +// name: 'category3', +// }, +// ], +// xSortedBy: ESortTypes.CAT_ASC, +// ySortedBy: ESortTypes.CAT_ASC, +// color: null, +// numColorScaleType: ENumericalColorScaleType.SEQUENTIAL, +// aggregateColumn: null, +// aggregateType: EAggregateTypes.COUNT, +// } as BaseVisConfig, +// };