Skip to content
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

feat: implement heatmap multiples #125

Open
wants to merge 3 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 40 additions & 39 deletions src/vis/heatmap/Heatmap.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -135,29 +135,29 @@ export function Heatmap({
: d3.extent(groupedVals, (d) => d.aggregateVal as number),
)
: config?.numColorScaleType === ENumericalColorScaleType.DIVERGENT
? d3
.scaleSequential<string, string>(
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<string, string>(
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,
Expand Down Expand Up @@ -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}`}
/>
Expand All @@ -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,
})
}
>
Expand All @@ -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}
Expand Down Expand Up @@ -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,
})
}
>
Expand All @@ -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}
Expand Down
47 changes: 40 additions & 7 deletions src/vis/heatmap/HeatmapGrid.tsx
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -21,9 +21,12 @@ 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 hasTwoCatCols = useMemo(() => allColumns?.catColumn && allColumns?.catColumn?.length === 2, [allColumns?.catColumn]);

const margin = React.useMemo(() => {
// 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 {
top: 10,
right: 20,
Expand All @@ -32,12 +35,17 @@ export function HeatmapGrid({
};
}, []);

// 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<ReturnType<typeof getHeatmapData>>['catColumn'][];
// }, [allColumns?.catColumn, hasTwoCatCols]);

return (
<Stack align="center" justify="center" sx={{ width: '100%', height: '100%' }} p="sm">
{status === 'pending' ? (
<Loader />
) : !hasAtLeast2CatCols ? (
<InvalidCols headerMessage="Invalid settings" bodyMessage="To create a heatmap chart, select at least 2 categorical columns." />
) : !hasTwoCatCols ? (
<InvalidCols headerMessage="Invalid settings" bodyMessage="To create a heatmap chart, select exactly 2 categorical columns." />
) : (
<Heatmap
column1={allColumns.catColumn[0]}
Expand All @@ -49,6 +57,31 @@ export function HeatmapGrid({
setExternalConfig={setExternalConfig}
selectionCallback={selectionCallback}
/>

// NOTE: @dv-usama-ansari: This implementation for multiple heatmaps works, but it's not very performant.
// <Box
// style={{
// display: 'grid',
// gridTemplateColumns: `repeat(${heatmapMultiples.length === 1 ? 1 : heatmapMultiples.length - 1}, 1fr)`,
// gridTemplateRows: `repeat(${heatmapMultiples.length === 1 ? 1 : heatmapMultiples.length - 1}, 1fr)`,
// width: '100%',
// height: '100%',
// }}
// >
// {heatmapMultiples.map(([column1, column2]) => (
// <Heatmap
// key={`${column1.info.id}-${column2.info.id}`}
// column1={column1}
// column2={column2}
// aggregateColumn={allColumns.aggregateColumn}
// margin={margin}
// config={config}
// selected={selected}
// setExternalConfig={setExternalConfig}
// selectionCallback={selectionCallback}
// />
// ))}
// </Box>
)}
</Stack>
);
Expand Down
10 changes: 10 additions & 0 deletions src/vis/heatmap/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,3 +50,13 @@ export async function getHeatmapData(

return { catColumn, aggregateColumn };
}

export const setsOfTwo = <T = unknown>(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;
};
10 changes: 7 additions & 3 deletions src/vis/legend/ColorLegend.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ export function ColorLegend({
format = '.3s',
rightMargin = 40,
title = null,
canvasIdentifier = '',
}: {
scale: (t: number) => string;
width?: number;
Expand All @@ -21,6 +22,7 @@ export function ColorLegend({
format?: string;
rightMargin?: number;
title: string;
canvasIdentifier?: string;
}) {
const colors = d3
.range(tickCount)
Expand All @@ -32,8 +34,10 @@ export function ColorLegend({

const canvasRef = useRef<HTMLCanvasElement>(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;
Expand All @@ -52,15 +56,15 @@ 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);
}, [format]);

return (
<Group spacing={5} noWrap style={{ width: `${width + rightMargin}px` }}>
<canvas id="proteomicsLegendCanvas" ref={canvasRef} />
<canvas id={canvasId} ref={canvasRef} />
<Stack align="stretch" justify="space-between" style={{ height: `${height}px` }} spacing={0} ml="0">
{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
Expand Down
10 changes: 7 additions & 3 deletions src/vis/legend/ColorLegendVert.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export function ColorLegendVert({
tickCount = 5,
title = null,
format = '.3s',
canvasIdentifier = '',
}: {
scale: (t: number) => string;
width?: number;
Expand All @@ -19,6 +20,7 @@ export function ColorLegendVert({
tickCount?: number;
title: string;
format?: string;
canvasIdentifier?: string;
}) {
const colors = d3
.range(tickCount)
Expand All @@ -30,8 +32,10 @@ export function ColorLegendVert({

const canvasRef = useRef<HTMLCanvasElement>(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;
Expand All @@ -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);
Expand All @@ -63,7 +67,7 @@ export function ColorLegendVert({
{title}
</Text>
) : null}
<canvas style={{ width: '100%' }} id="proteomicsLegendCanvas" ref={canvasRef} />
<canvas style={{ width: '100%' }} id={canvasId} ref={canvasRef} />

<Group position="apart" style={{ width: `100%` }} spacing={0} ml="0">
{colors.map((color, i) => (
Expand Down
77 changes: 40 additions & 37 deletions src/vis/stories/Vis/Heatmap/HeatmapRandom.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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,
// };
Loading